package com.boardgamegeek.ui; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener; import android.support.v7.graphics.Palette; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.AbsListView; import android.widget.AbsListView.LayoutParams; import android.widget.AbsListView.OnScrollListener; import android.widget.BaseAdapter; import android.widget.Chronometer; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.boardgamegeek.R; import com.boardgamegeek.events.PlayDeletedEvent; import com.boardgamegeek.events.PlaySentEvent; import com.boardgamegeek.events.UpdateCompleteEvent; import com.boardgamegeek.events.UpdateEvent; import com.boardgamegeek.model.Play; import com.boardgamegeek.model.Player; import com.boardgamegeek.model.builder.PlayBuilder; import com.boardgamegeek.model.persister.PlayPersister; import com.boardgamegeek.provider.BggContract; import com.boardgamegeek.provider.BggContract.Plays; import com.boardgamegeek.service.UpdateService; import com.boardgamegeek.ui.widget.PlayerRow; import com.boardgamegeek.ui.widget.TimestampView; import com.boardgamegeek.util.ActivityUtils; import com.boardgamegeek.util.DateTimeUtils; import com.boardgamegeek.util.DialogUtils; import com.boardgamegeek.util.ImageUtils; import com.boardgamegeek.util.ImageUtils.Callback; import com.boardgamegeek.util.NotificationUtils; import com.boardgamegeek.util.PresentationUtils; import com.boardgamegeek.util.UIUtils; import com.boardgamegeek.util.fabric.PlayManipulationEvent; import com.crashlytics.android.answers.Answers; import com.crashlytics.android.answers.ShareEvent; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.Unbinder; import hugo.weaving.DebugLog; import icepick.Icepick; import icepick.State; public class PlayFragment extends ListFragment implements LoaderCallbacks<Cursor>, OnRefreshListener { private static final int AGE_IN_DAYS_TO_REFRESH = 7; private static final int PLAY_QUERY_TOKEN = 0x01; private static final int PLAYER_QUERY_TOKEN = 0x02; private boolean isSyncing; private long internalId = BggContract.INVALID_ID; private Play play = new Play(); private String thumbnailUrl; private String imageUrl; private Unbinder unbinder; private ListView playersView; @BindView(R.id.swipe_refresh) SwipeRefreshLayout swipeRefreshLayout; @BindView(R.id.progress) View progressContainer; @BindView(R.id.list_container) View listContainer; @BindView(R.id.empty) TextView emptyView; @BindView(R.id.thumbnail) ImageView thumbnailView; @BindView(R.id.header) TextView gameNameView; @BindView(R.id.play_date) TextView dateView; @BindView(R.id.play_quantity) TextView quantityView; @BindView(R.id.length_root) View lengthContainer; @BindView(R.id.play_length) TextView lengthView; @BindView(R.id.timer_root) View timerContainer; @BindView(R.id.timer) Chronometer timerView; @BindView(R.id.location_root) View locationContainer; @BindView(R.id.play_location) TextView locationView; @BindView(R.id.play_incomplete) View incompleteView; @BindView(R.id.play_no_win_stats) View noWinStatsView; @BindView(R.id.play_comments) TextView commentsView; @BindView(R.id.play_comments_label) View commentsLabel; @BindView(R.id.play_players_label) View playersLabel; @BindView(R.id.play_id) TextView playIdView; @BindView(R.id.pending_timestamp) TimestampView pendingTimestampView; @BindView(R.id.dirty_timestamp) TimestampView dirtyTimestampView; @BindView(R.id.sync_timestamp) TimestampView syncTimestampView; private PlayerAdapter adapter; @State boolean hasBeenNotified; final private OnScrollListener onScrollListener = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (swipeRefreshLayout != null) { int topRowVerticalPosition = (view == null || view.getChildCount() == 0) ? 0 : view.getChildAt(0).getTop(); swipeRefreshLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0); } } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState); setHasOptionsMenu(true); final Intent intent = UIUtils.fragmentArgumentsToIntent(getArguments()); internalId = intent.getLongExtra(ActivityUtils.KEY_ID, BggContract.INVALID_ID); if (internalId == BggContract.INVALID_ID) return; play = new Play(intent.getIntExtra(ActivityUtils.KEY_GAME_ID, BggContract.INVALID_ID), intent.getStringExtra(ActivityUtils.KEY_GAME_NAME)); thumbnailUrl = intent.getStringExtra(ActivityUtils.KEY_THUMBNAIL_URL); imageUrl = intent.getStringExtra(ActivityUtils.KEY_IMAGE_URL); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_play, container, false); playersView = (ListView) rootView.findViewById(android.R.id.list); playersView.setHeaderDividersEnabled(false); playersView.setFooterDividersEnabled(false); playersView.addHeaderView(View.inflate(getActivity(), R.layout.header_play, null), null, false); playersView.addFooterView(View.inflate(getActivity(), R.layout.footer_play, null), null, false); unbinder = ButterKnife.bind(this, rootView); if (swipeRefreshLayout != null) { swipeRefreshLayout.setOnRefreshListener(this); swipeRefreshLayout.setColorSchemeResources(PresentationUtils.getColorSchemeResources()); } adapter = new PlayerAdapter(); playersView.setAdapter(adapter); getLoaderManager().restartLoader(PLAY_QUERY_TOKEN, null, this); return rootView; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getListView().setOnScrollListener(onScrollListener); } @DebugLog @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onResume() { super.onResume(); if (play != null && play.hasStarted()) { showNotification(); hasBeenNotified = true; } } @DebugLog @Override public void onStop() { EventBus.getDefault().unregister(this); super.onStop(); } @Override public void onDestroyView() { super.onDestroyView(); if (unbinder != null) unbinder.unbind(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.play, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { UIUtils.showMenuItem(menu, R.id.menu_send, play.dirtyTimestamp > 0); UIUtils.showMenuItem(menu, R.id.menu_discard, play.playId > 0 && play.dirtyTimestamp > 0); UIUtils.enableMenuItem(menu, R.id.menu_share, play.playId > 0); super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_discard: DialogUtils.createConfirmationDialog(getActivity(), R.string.are_you_sure_refresh_message, new OnClickListener() { public void onClick(DialogInterface dialog, int id) { play.dirtyTimestamp = 0; play.updateTimestamp = 0; play.deleteTimestamp = 0; save("Discard"); } }).show(); return true; case R.id.menu_edit: PlayManipulationEvent.log("Edit", play.gameName); ActivityUtils.editPlay(getActivity(), internalId, play.gameId, play.gameName, thumbnailUrl, imageUrl); return true; case R.id.menu_send: play.updateTimestamp = System.currentTimeMillis(); save("Save"); EventBus.getDefault().post(new PlaySentEvent()); return true; case R.id.menu_delete: { DialogUtils.createConfirmationDialog(getActivity(), R.string.are_you_sure_delete_play, new OnClickListener() { public void onClick(DialogInterface dialog, int id) { if (play.hasStarted()) { cancelNotification(); } play.end(); // this prevents the timer from reappearing play.deleteTimestamp = System.currentTimeMillis(); save("Delete"); EventBus.getDefault().post(new PlayDeletedEvent()); } }).show(); return true; } case R.id.menu_rematch: PlayManipulationEvent.log("Rematch", play.gameName); ActivityUtils.rematch(getActivity(), internalId, play.gameId, play.gameName, thumbnailUrl, imageUrl); getActivity().finish(); // don't want to show the "old" play upon return return true; case R.id.menu_share: ActivityUtils.share(getActivity(), play.toShortDescription(getActivity()), play.toLongDescription(getActivity()), R.string.share_play_title); Answers.getInstance().logShare(new ShareEvent() .putContentType("Play") .putContentName(play.toShortDescription(getActivity())) .putContentId(String.valueOf(play.playId))); return true; } return super.onOptionsItemSelected(item); } @DebugLog @Override public void onRefresh() { triggerRefresh(); } @SuppressWarnings("unused") @DebugLog @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void onEvent(UpdateEvent event) { if (event.getType() == UpdateService.SYNC_TYPE_GAME_PLAYS) { isSyncing = true; updateRefreshStatus(); } } @SuppressWarnings({ "unused", "UnusedParameters" }) @DebugLog @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void onEvent(UpdateCompleteEvent event) { isSyncing = false; updateRefreshStatus(); } @SuppressWarnings("unused") @DebugLog private void updateRefreshStatus() { if (swipeRefreshLayout != null) { swipeRefreshLayout.post(new Runnable() { @Override public void run() { if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(isSyncing); } }); } } @OnClick(R.id.header_container) void viewGame() { ActivityUtils.launchGame(getActivity(), play.gameId, play.gameName); } @OnClick(R.id.timer_end) void onTimerClick() { ActivityUtils.endPlay(getActivity(), internalId, play.gameId, play.gameName, thumbnailUrl, imageUrl); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle data) { CursorLoader loader = null; switch (id) { case PLAY_QUERY_TOKEN: loader = new CursorLoader(getActivity(), Plays.buildPlayUri(internalId), PlayBuilder.PLAY_PROJECTION, null, null, null); break; case PLAYER_QUERY_TOKEN: loader = new CursorLoader(getActivity(), Plays.buildPlayerUri(internalId), PlayBuilder.PLAYER_PROJECTION, null, null, null); break; } if (loader != null) { loader.setUpdateThrottle(1000); } return loader; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { if (getActivity() == null) { return; } switch (loader.getId()) { case PLAY_QUERY_TOKEN: if (onPlayQueryComplete(cursor)) { showList(); } break; case PLAYER_QUERY_TOKEN: PlayBuilder.addPlayers(cursor, play); playersLabel.setVisibility(play.getPlayers().size() == 0 ? View.GONE : View.VISIBLE); adapter.notifyDataSetChanged(); maybeShowNotification(); showList(); break; default: if (cursor != null) { cursor.close(); } break; } } private void showList() { progressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_out)); progressContainer.setVisibility(View.GONE); listContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); listContainer.setVisibility(View.VISIBLE); } @Override public void onLoaderReset(Loader<Cursor> loader) { } /** * @return true if the we're done loading */ private boolean onPlayQueryComplete(Cursor cursor) { if (cursor == null || !cursor.moveToFirst()) { emptyView.setText(String.format(getResources().getString(R.string.empty_play), String.valueOf(internalId))); emptyView.setVisibility(View.VISIBLE); return true; } playersView.setVisibility(View.VISIBLE); emptyView.setVisibility(View.GONE); ImageUtils.safelyLoadImage(thumbnailView, imageUrl, new Callback() { @Override public void onSuccessfulImageLoad(Palette palette) { if (gameNameView != null && isAdded()) { gameNameView.setBackgroundResource(R.color.black_overlay_light); } } @Override public void onFailedImageLoad() { } }); List<Player> players = play.getPlayers(); play = PlayBuilder.fromCursor(cursor); play.setPlayers(players); gameNameView.setText(play.gameName); dateView.setText(play.getDateForDisplay(getActivity())); quantityView.setText(getResources().getQuantityString(R.plurals.times_suffix, play.quantity, play.quantity)); quantityView.setVisibility((play.quantity == 1) ? View.GONE : View.VISIBLE); if (play.length > 0) { lengthContainer.setVisibility(View.VISIBLE); lengthView.setText(DateTimeUtils.describeMinutes(getActivity(), play.length)); lengthView.setVisibility(View.VISIBLE); timerContainer.setVisibility(View.GONE); timerView.stop(); } else if (play.hasStarted()) { lengthContainer.setVisibility(View.VISIBLE); lengthView.setVisibility(View.GONE); timerContainer.setVisibility(View.VISIBLE); UIUtils.startTimerWithSystemTime(timerView, play.startTime); } else { lengthContainer.setVisibility(View.GONE); } locationView.setText(play.location); locationContainer.setVisibility(TextUtils.isEmpty(play.location) ? View.GONE : View.VISIBLE); incompleteView.setVisibility(play.Incomplete() ? View.VISIBLE : View.GONE); noWinStatsView.setVisibility(play.NoWinStats() ? View.VISIBLE : View.GONE); commentsView.setText(play.comments); commentsView.setVisibility(TextUtils.isEmpty(play.comments) ? View.GONE : View.VISIBLE); commentsLabel.setVisibility(TextUtils.isEmpty(play.comments) ? View.GONE : View.VISIBLE); if (play.deleteTimestamp > 0) { pendingTimestampView.setVisibility(View.VISIBLE); pendingTimestampView.setFormat(R.string.delete_pending_prefix); pendingTimestampView.setTimestamp(play.deleteTimestamp); } else if (play.updateTimestamp > 0) { pendingTimestampView.setVisibility(View.VISIBLE); pendingTimestampView.setFormat(R.string.update_pending_prefix); pendingTimestampView.setTimestamp(play.updateTimestamp); } else { pendingTimestampView.setVisibility(View.GONE); } if (play.dirtyTimestamp > 0) { dirtyTimestampView.setFormat(getString(play.playId > 0 ? R.string.editing_prefix : R.string.draft_prefix)); dirtyTimestampView.setTimestamp(play.dirtyTimestamp); } else { dirtyTimestampView.setVisibility(View.GONE); } if (play.playId > 0) { playIdView.setText(String.format(getResources().getString(R.string.play_id_prefix), String.valueOf(play.playId))); } syncTimestampView.setTimestamp(play.syncTimestamp); getActivity().supportInvalidateOptionsMenu(); getLoaderManager().restartLoader(PLAYER_QUERY_TOKEN, null, this); if (play.playId > 0 && (play.syncTimestamp == 0 || DateTimeUtils.howManyDaysOld(play.syncTimestamp) > AGE_IN_DAYS_TO_REFRESH)) { triggerRefresh(); } return false; } private void maybeShowNotification() { if (play.hasStarted()) { showNotification(); } else if (hasBeenNotified) { cancelNotification(); } } private void showNotification() { NotificationUtils.launchPlayingNotification(getActivity(), internalId, play, thumbnailUrl, imageUrl); } private void cancelNotification() { NotificationUtils.cancel(getActivity(), NotificationUtils.TAG_PLAY_TIMER, internalId); } private void triggerRefresh() { UpdateService.start(getActivity(), UpdateService.SYNC_TYPE_GAME_PLAYS, play.gameId); } private void save(String action) { PlayManipulationEvent.log(TextUtils.isEmpty(action) ? "Save" : action, play.gameName); new PlayPersister(getActivity()).save(play, internalId, false); triggerRefresh(); } private class PlayerAdapter extends BaseAdapter { @Override public boolean isEnabled(int position) { return false; } @Override public int getCount() { return play.getPlayerCount(); } @Override public Object getItem(int position) { return play.getPlayers().get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, final View convertView, ViewGroup parent) { PlayerRow row = new PlayerRow(getActivity()); row.setLayoutParams(new ListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); final Player player = (Player) getItem(position); row.setPlayer(player); row.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ActivityUtils.startBuddyActivity(getActivity(), player.username, player.name); } }); return row; } } }